{-# OPTIONS_GHC -Wall -Werror #-}
{-# LANGUAGE FlexibleInstances #-}

module Math 
( Scalar       
, Time         
, Duration     
, Speed        
, Mass         
, Weight       
, Radius       
, Length       
, Distance     
, Angle        
, Modulo(..)
, clamped
, wrapped
, isBetween
, sign
, toDegrees
, toRadians
, showN
)
where

import Data.Fixed
import Data.VectorSpace hiding (Scalar)

type Scalar       = Double
type Time         = Scalar
type Duration     = Scalar
type Speed        = Scalar
type Mass         = Scalar
type Weight       = Scalar
type Radius       = Scalar
type Length       = Scalar
type Distance     = Scalar
type Angle        = Scalar -- radians

class Modulo a where
  modulo :: a -> a -> a
  -- strictModulo n d always returns a value m with 0 <= m < d
  strictModulo :: a -> a -> a

numericStrictModulo :: (Modulo a, Num a, Ord a) => a -> a -> a
numericStrictModulo n d = let m = modulo n d in if m>=0 then m else m+d

instance Modulo Float where
  modulo = mod'
  strictModulo = numericStrictModulo

instance Modulo Double where
  modulo = mod'
  strictModulo = numericStrictModulo

instance Modulo Int where
  modulo = mod
  strictModulo = numericStrictModulo

instance Modulo Integer where
  modulo = mod
  strictModulo = numericStrictModulo

instance Modulo v => Modulo (v,v) where
  modulo (nx,ny) (dx,dy) = (modulo nx dx, modulo ny dy)
  strictModulo (nx,ny) (dx,dy) = (strictModulo nx dx, strictModulo ny dy) 

clamped :: Ord a => a -> a -> a -> a
clamped v lo hi = max (min v hi) lo

-- Wrap a cyclic value x with lower bounds <lo> and higher bounds <hi>.
-- For example for the hours of a clock, lo=0 and hi=12.
-- The returned value will be between lo (inclusive) and hi (exclusive) 
wrapped :: (AdditiveGroup v, Modulo v) => v -> v -> v -> v
wrapped x lo hi = lo ^+^ strictModulo (x^-^lo) (hi^-^lo)

-- Returns the sign of n.
-- This is -1 for strictly negative numbers,
-- otherwise +1 (for positive numbers or zero).
sign :: (Num a, Ord a) => a -> Int
sign n = if n>=0 then 1 else (-1)

toDegrees :: Scalar -> Scalar
toDegrees rad = 180 * rad / pi

toRadians :: Scalar -> Scalar
toRadians deg = pi * deg / 180

isBetween :: Ord a => a -> a -> a -> Bool
isBetween lo hi x = x >= lo && x <= hi

roundTo :: Int -> Scalar -> Scalar
roundTo digits x = realToFrac (round (x * s)) / s
                   where 
                     s = 10^digits

showN :: Scalar -> String
showN x = show (roundTo 3 x)
